Linux系统下深究一个malloc/brk/sbrk新内存后的page fault问题 |
您所在的位置:网站首页 › malloc 内存对齐 › Linux系统下深究一个malloc/brk/sbrk新内存后的page fault问题 |
有耳可听的,就应当听 —《马可福音》 周四的休假团建又没有去,不因别的,只因年前东北行休假太多了,想缓缓…不过真实原因也确实因为假期剩余无几了…思考了一些问题,写下本文。 本文的缘起来自于和同事讨论一个关于缺页中断按需调页的讨论。真可谓是三人行必有我师,最近经常能从一些随意的比划或招架中悟出一丝意义,所以非常感谢周围的信息输出者!甚至从小小学校全员禁言的作业群里,我都能每天重温一首古诗词,然后循此生意,去故意制造另一种真实的意境,然后发个朋友圈?~ 感谢大家的信息输入,每次收到的好玩的东西,我都会即时整理并重新再输出。 内容简介本文描述了一个非常显然但却又很少有人知道其所以然的问题,更重要的是分享一种解决问题的思路。PS:这个问题非常好玩。 不搞悬念,本文解释一个事实,即匿名页缺页中断数量和物理页面的分配数量并不是一致的。即便不考虑共享内存的影响,也并非发生一次匿名页缺页中断,就一定会分配一个独立的物理页面。 问题问题很简单,我把问题抽象成了下面的代码: #include #include #include #include #define SIZE 100char *addrs[SIZE];char dest[4096][SIZE];int main() { int i; for (i = 0; i pte 0000000037318067 # 翻译PUD表项 PTE PHYSICAL FLAGS37318067 37318000 (PRESENT|RW|USER|ACCESSED|DIRTY)step 4:循着PUD表项读出PMD表项: # 这里不再给注释crash> px (0xc7e000>>21) & 0x1ff$6 = 0x6 crash> px 0x37318000+$6*8$7 = 0x37318030 crash> rd -p 0x37318030 37318030: 000000003883d067 g..8.... crash> pte 000000003883d067 PTE PHYSICAL FLAGS3883d067 3883d000 (PRESENT|RW|USER|ACCESSED|DIRTY)step 5:循着PMD表项读出PTE: crash> px (0xc7e000>>12) & 0x1ff$8 = 0x7e crash> px 0x3883d000+$8*8$9 = 0x3883d3f0 crash> rd -p 0x3883d3f0 3883d3f0: 800000003b9e3867 g8.;.... crash> pte 800000003b9e3867 PTE PHYSICAL FLAGS800000003b9e3867 3b9e3000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX)此时的地址0x3b9e3000就是物理页面的位置了,由于我们brk申请了整个页面,因此页面偏移为0,接下来就是直接dump内存了。(严谨点讲,需要用 addr&0x0fff 找出页面偏移的,但是0xc7e000这个地址是页面对齐的,也就不再费事了!) step 6:dump整个页面的内存: crash> rd -p 0x3b9e3000 3b9e3000: 6161616161616161 aaaaaaaa哇! step 7:看看p2对应的页表 由于p2和p3在虚拟地址上相邻4096个字节(一个页面),因此只需要做最后一步即可,即将p3的PTE索引向前移1个单位。但是我们依然按照正规的方式来一遍: crash> px (0xc7d000>>12) & 0x1ff$10 = 0x7d crash> px 0x3883d000+$10*8$11 = 0x3883d3e8 crash> rd -p 0x3883d3e8 3883d3e8: 0000000000000000 ........ crash> pte 0000000000000000 PTE PHYSICAL FLAGS 0 0 (no mapping) # nothing!!连页表项都没有,何来的内容!请问何来的内容?!现在让我们的a.out向前一步走,即在a.out的运行终端敲入回车,再次dump p2的页表项。 step 8:a.out读取一下p2指针4096字节范围内的内容之后再次看p2的PTE: crash> rd -p 0x3883d3e8 3883d3e8: 800000003df22225 crash> pte 800000003df22225 PTE PHYSICAL FLAGS800000003df22225 3df22000 (PRESENT|USER|ACCESSED|NX)看看吧,只要读了一下p2的内容,PTE就Present了!这个时候,我们可以读一下其内容: crash> rd -p 0x3df22000 3df22000: 0000000000000000 ........内容为全0! step 9:让a.out更进一步,拷贝’b’到p2后再次读取内容: 在a.out的终端上敲入回车,然后看PTE指示页面的内容: crash> rd -p 0x3df22000 3df22000: 0000000000000000 ........这是为什么?为什么没有显示’b’字符,为什么还是全0?为此,不得不把最后一步重新来一遍了,即从读取PTE开始: crash> px (0xc7d000>>12) & 0x1ff$13 = 0x7d crash> px 0x3883d000+$13*8 # 循着PMD找到PTE$14 = 0x3883d3e8 crash> rd -p 0x3883d3e8 3883d3e8: 800000002dac8867 # 注意!这个PTE和之前不同了crash> pte 800000002dac8867 PTE PHYSICAL FLAGS800000002dac8867 2dac8000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) crash> rd -p 0x2dac8000 # 对p2进行写操作前后,其PTE指示的页面不同了!! 2dac8000: 6262626262626262 bbbbbbbb重做一遍终于还是找到了,过程中PTE发生了变化。可以用vtop直接dump p2的虚拟地址看一下: crash> vtop -c 1764 -u 0xc7d000 VIRTUAL PHYSICAL c7d000 2dac8000 PML: 38e20000 => 8000000037f4d067 PUD: 37f4d000 => 37318067 PMD: 37318030 => 3883d067 PTE: 3883d3e8 => 800000002dac8867 PAGE: 2dac8000 PTE PHYSICAL FLAGS800000002dac8867 2dac8000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) VMA START END FLAGS FILE ffff9be1eb39f440 c7d000 c7f000 8100073 PAGE PHYSICAL MAPPING INDEX CNT FLAGS fffff3b700b6b200 2dac8000 ffff9be1c7c7eaf1 c7d 1 1fffff00080068 uptodate,lru,active,swapbacked# 结果正是2dac8000!和手工dump的结果完全一致!我们前面绕了那么大一圈,其实直接用vtop命令就可以把整个MMU转换过程看得一清二楚。但是通过上述手工dump PTE的过程,更加熟悉了不是吗? 但是这里出现一个问题!为什么在对p2进行只读操作时,和对它进行写入之后,其PTE指示的物理页面是不同的页面? # 对p2内存只读操作之后 crash> rd -p 0x3883d3e8 3883d3e8: 800000003df22225 ———————————————— # 对p2内存写操作之后 crash> rd -p 0x3883d3e8 3883d3e8: 800000002dac8867 此外,其PTE对应的flags也不同,对其只读的时候,没有置入RW标志,即此时该页面是不可写的。 是时候揭示谜底了! 在对新分配的虚拟地址空间第一次读操作时,page fault确实会调入一个页面,然而对于这种读操作,所有的进程调入的都是同一个zeropage页面。对于这种第一次读操作,内核会将这同一个zeropage映射给被读的虚拟地址页面。这最大限度地发挥了Lazy策略的品性! 内核代码解释这一切(what->how)到目前,我们已经知道所有事实,从用户态统计到内核crash工具分析,然而要想知道内核是如何做的,即从waht导出how,就要看一眼内核源码了,这里的目标非常明确,直接看do_anonymous_page的逻辑即可: static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, pte_t *page_table, pmd_t *pmd, unsigned int flags) { ... /* Use the zero-page for reads */ if (!(flags & FAULT_FLAG_WRITE)) { // 只读情况直接从zeropage里拿即可。 entry = pte_mkspecial(pfn_pte(my_zero_pfn(address), vma->vm_page_prot)); page_table = pte_offset_map_lock(mm, pmd, address, &ptl); if (!pte_none(*page_table)) goto unlock; goto setpte; } ... // 非只读,才会实际从物理池里分配页面 page = alloc_zeroed_user_highpage_movable(vma, address); ... // 如果读fault映射了zeropage,将不会递增AnonRSS计数器。 inc_mm_counter_fast(mm, MM_ANONPAGES); page_add_new_anon_rmap(page, vma, address); setpte: ... }我们看看什么是zeropage: /* * ZERO_PAGE is a global shared page that is always zero: used * for zero-mapped memory areas etc.. */extern unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)];#define ZERO_PAGE(vaddr) (virt_to_page(empty_zero_page))...static int __init init_zero_pfn(void) { zero_pfn = page_to_pfn(ZERO_PAGE(0)); return 0; } ...static inline unsigned long my_zero_pfn(unsigned long addr) { extern unsigned long zero_pfn; return zero_pfn; }所有谜底已经揭开!从一个实验用例,到统计分析,到crash工具分析内核状态,到内核源码确认,这就完成了一个解决问题的闭环,但貌似还缺少点什么…接下来还有一个形而上的分析。 how->why之所以将第一次以read方式touch到的虚拟地址空间对应的物理页面映射到一个全局的zeropage,是在按需调页更进一步地加强了Lazy品性!从而更加有效地落实写时拷贝策略,将不得已而分配的物理页面真真地推迟到最后那一刻,从而将无谓的浪费行为降低到最少! 如果说按需调页的page fault机制已经实现了Lazy品性,那么深究起来它做得还不够好,说它做得不够好是因为page fault机制忽略了按需调页两个层面中的一面: 当touch一个从未touch过的虚拟页面的时候,需要调入一个物理页面; 当调入一个物理页面时,是不是可以和其它的进程共享该物理页面; 第1点说的是按需分配,不得已时才分配,page fault做到了(注意,PTE本身也是按需调入的),第2点说的是尽力压缩,非要分配时,能不能尽量少分配,两者都做到了,Lazy策略才能达到真正按需调页思路的极致。 结论 & 评价结论很明确: Linux系统并不会对新分配未touch的虚拟内存映射任何物理页面; 以read方式首次touch时会映射共享的zeropage页面; 以write方式首次touch时会分配新的物理页面并映射之; 以write方式在首次read touch之后touch时,写时拷贝会分配新的物理页面并映射之。 为什么是这样可以考虑以下两点: 基于虚拟地址空间的操作系统内存子系统采用的是按需调页策略,这是设计决定的。 参考Linux内核的实现,Linux page fault处理区分对待了匿名页的read fault和write fault。 附:关于PTE的按需调入在进程新fork出来初始化时,mm_init函数中会调用pgd_alloc,在pgd_alloc中会处理pgd的prepopulate,但是没有pte的populate,pte的populate是在do_page_fault中处理的,可见由于内存分布的局部稠密性和全局稀疏性,PTE的Lazy分配时必要的。 其次,我们看C库增持内存池内存需要通过系统调用进一步调用到的do_brk以及do_mmap,除非你使用了VM_LOCKED,否则就不会prepopulate任何页面。上面两点保证了至少在X86的32位/64位平台,不会对用户地址空间的虚拟内存有任何可读的预映射页面。 若要观测PTE本身的按需调入,参考下面的代码: #include #include #include int main(int argc, char **argv) { char *p1, *p2; char c; int i; p1 = sbrk(0); // 让p2离开更远的距离,防止p2的PTE和已分配的PTE在一个page内! for (i = 0; i |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |